export class JCEngine {
    public static entityClass: new() => JCEntity;

    public static boot(url: string, entityClass: new() => JCEntity) {
        this.entityClass = entityClass;
        new WebSocketServer(url);
    }
}

export class JCEntity {
    public id: number;
    public channel: JCChannel;
    public isValid: boolean;

    public onLoad() {}

    public onDestroy() {}

    public onMiss() {}

    public call(func: string, args?: any[], callback?: Function) {
        if (this.isValid) {
            let uuid = "";
            let type = JCDataType.FUNCTION;
            if (func.indexOf(".") > -1) {
                type = JCDataType.METHOD;
                if (!callback) {
                    callback = arguments[arguments.length - 1];
                }
                uuid = CallbackHandler.addCallback(callback);
            }
            if (args == undefined) {
                args = [];
            }
            let data = {uuid: uuid, type: type, func: func, args: args};
            this.channel.writeAndFlush(JSON.stringify(data));
        }
    }
}

class JCChannel {
    private webSocket: WebSocket;

    constructor(webSocket: WebSocket) {
        this.webSocket = webSocket;
    }

    public writeAndFlush(text: string) {
        this.webSocket.send(text);
    }

    public close() {
        this.webSocket.close();
    }
}

class WebSocketServer {
    private webSocket: WebSocket;
    private tempEntity: JCEntity;

    constructor(url: string) {
        this.webSocket = new WebSocket(url);
        this.tempEntity = new JCEngine.entityClass();

        this.webSocket.onopen = () => {
            this.call("loadTempEntity");
        }

        this.webSocket.onclose = () => {
            this.destroyTempEntity();
        }

        this.webSocket.onmessage = (event: MessageEvent) => {
            this.invoke(JSON.parse(event.data));            
        }
    }

    private call(func: string, args?: any[]) {
        if (args == undefined) {
            args = [];
        }
        let data: JCData = {uuid: "", type: JCDataType.EVENT, func: func, args: args};
        this.webSocket.send(JSON.stringify(data));
    }

    private invoke(data: JCData) {
        if (data.type == JCDataType.EVENT) {
            this[data.func].apply(this, data.args);
            return;
        }
        if (data.type == JCDataType.FUNCTION) {
            if (this.tempEntity.isValid) {
                this.tempEntity[data.func].apply(this.tempEntity, data.args); 
            }
            return;
        }
        if (data.type == JCDataType.METHOD) {
            CallbackHandler.handleCallback(data);
        }
    }

    public loadTempEntity(id: number) {
        this.tempEntity.id = id;
        this.tempEntity.channel = new JCChannel(this.webSocket);
        this.tempEntity.isValid = true;
        this.tempEntity.onLoad();
    }

    public destroyTempEntity() {
        if (this.tempEntity.isValid) {
            this.tempEntity.isValid = false;
            this.tempEntity.onDestroy();            
        } else {
            this.tempEntity.onMiss();
        }
    }
}

class CallbackHandler {
    private static nextID: number = 0;
    private static mapper: Map<string, CallbackInfo> = new Map();

    private static uuid(): string {
        this.nextID++;
        return this.nextID.toString();
    }

    public static addCallback(callback: Function): string {
        let uuid = this.uuid();
        if (callback instanceof Function) {
            this.mapper.set(uuid, {
                callback: callback, 
                deadTime: Date.now() + 10 * 1000
            });            
        }
        return uuid;
    }

    public static handleCallback(data: JCData) {
        if (this.mapper.size > 10) {
            let now = Date.now();
            for (let item of (this.mapper as any)) {
                let key = item[0].deadTime;
                let value = item[1];
                if (now >= value.deadTime) {
                    this.mapper.delete(key);
                }
            }
        }
        let callbackInfo = this.mapper.get(data.uuid);
        if (callbackInfo && callbackInfo.callback instanceof Function) {
            this.mapper.delete(data.uuid);
            callbackInfo.callback(data.args[0]);
        }
    }
} 

interface CallbackInfo {
    callback: Function;
    deadTime: number;
}

interface JCData {
    uuid: string;
    type: number;
    func: string;
    args: any[];
}

enum JCDataType {
    EVENT,
    FUNCTION,
    METHOD
}